/**
 * 
 */
package info.niwota.web.apache;

import info.niwota.commons.MimeTypes;
import info.niwota.commons.io.Files;
import info.niwota.web.Debug;
import info.niwota.web.NVPair;
import info.niwota.web.R;
import info.niwota.web.Session;
import info.niwota.web.SessionManager;
import info.niwota.web.WebContentProvider;
import info.niwota.web.peer.PeerHttpClient;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.SequenceInputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import jvs.peer.io.ChannelManager;

import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import x.org.apache.http.Header;
import x.org.apache.http.HttpRequest;
import x.org.apache.http.HttpResponse;
import x.org.apache.http.HttpStatus;
import x.org.apache.http.entity.ContentProducer;
import x.org.apache.http.entity.EntityTemplate;
import x.org.apache.http.entity.FileEntity;
import x.org.apache.http.entity.InputStreamEntity;
import x.org.apache.http.entity.StringEntity;
import x.org.apache.http.protocol.HttpContext;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources.NotFoundException;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

/**
 * @author qiangli
 */
abstract public class BaseHandler implements Handler, MimeTypes {
	protected static final SimpleDateFormat RFC1123_DF = new SimpleDateFormat(
			"EEE, dd MMM yyyy HH:mm:ss 'GMT'");

	protected static final String SESSION_ID_KEY = SessionManager.SESSION_ID_KEY;

	protected static final String SESSION_KEY = SessionManager.SESSION_KEY;

	protected static final String SESSION_HEADER_KEY = "Blueweb-Session-ID"; //
	
	public static final String FIX_JSON_KEY = "fix_json";

	protected static final int BUFSIZE = 1024;

	public static final String PARAMETERS_KEY = "parameters";

	public static final String UTF8 = "UTF-8";

	private static final String CONTENT_TEXT_HTML = "text/html; charset=UTF-8";

	private static final String TAG = "BaseHandler";

	private static final String STATUS_ = "STATUS-";

	protected PeerHttpClient blueclient = new PeerHttpClient(ChannelManager.NETWORK_BTSPP);
	protected PeerHttpClient jabclient = new PeerHttpClient(ChannelManager.NETWORK_XMPP);

	protected Context ctx;

	public BaseHandler(Context ctx) {
		this.ctx = ctx;
	}

	/**
	 * 
	 * Extract return content type specified by query param "format"
	 * 
	 * @param path
	 * @return
	 */
	private String getFormat(String path) {
		Uri u = Uri.parse((path.indexOf("://") == -1 ? "x://" : "") + path);
		String t = u.getQueryParameter("format");
		if (Debug.DEBUG) {
			Log.d(TAG, "getFormat " + t + " uri: " + path);
		}
		return t;
	}

	protected String format(String status, String resson, String contentType) {
		contentType = (isNull(contentType) ? TEXT_HTML : contentType);
		StringBuilder sb = new StringBuilder();
		if (contentType.equals(APPLICATION_JSON)) {
			JSONObject jo = new JSONObject();
			try {
				jo.put("status", HttpStatus.SC_NOT_FOUND + "");
				jo.put("reason", resson);
			} catch (JSONException e) {
				e.printStackTrace();
			}
			sb.append(jo.toString());
		} else if (contentType.equals(TEXT_XML)
				|| contentType.equals(APPLICATION_XML)) {
			sb.append("<message>");
			sb.append("<status>");
			sb.append(status);
			sb.append("</status>");
			sb.append("<reason>");
			sb.append(resson);
			sb.append("</reason>");
			sb.append("</message>");
		} else {
			// text/plain
			sb.append(status);
			sb.append(": ");
			sb.append(resson);
		}
		return sb.toString();
	}

	protected void sendError(HttpRequest req, HttpResponse res,
			final Exception e) throws UnsupportedEncodingException {
		String type = getFormat(req.getRequestLine().getUri());
		String msg = format(STATUS_ + HttpStatus.SC_INTERNAL_SERVER_ERROR,
				toString(e), type);
		res.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
		StringEntity body = new StringEntity(msg, type, "UTF-8");
		res.setEntity(body);
	}

	protected String toString(Exception e) {
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		e.printStackTrace(pw);

		return sw.toString();
	}

	protected void sendNotFound(HttpRequest req, HttpResponse res,
			final String msg) throws Exception {
		String type = getFormat(req.getRequestLine().getUri());
		String s = format(STATUS_ + HttpStatus.SC_NOT_FOUND, msg, type);

		StringEntity body = new StringEntity(s, type, "UTF-8");
		res.setStatusCode(HttpStatus.SC_NOT_FOUND);
		res.setEntity(body);
	}

	protected String getMethod(HttpRequest req) {
		String method = req.getRequestLine().getMethod()
				.toUpperCase(Locale.ENGLISH);
		return method;
	}

	protected void sendContent(HttpRequest req, HttpResponse res, HttpContext context, String s, String mimeType,
			String encoding) throws Exception {
		mimeType = (isNull(mimeType) ? "text/html" : mimeType);
		encoding = (isNull(encoding) ? "UTF-8" : encoding);
		
		StringEntity body = (mimeType.equals(APPLICATION_JSON) ? createJsonEntity(req, res, context, s) : new StringEntity(s, mimeType, encoding));

		res.setStatusCode(HttpStatus.SC_OK);
		res.setEntity(body);
	}

	private boolean isNull(String s) {
		return (s == null || s.equals("") || s.equalsIgnoreCase("null"));
	}

	protected void sendContent(HttpRequest req, HttpResponse res, final InputStream in,
			String mimeType, int length) throws IOException {
		res.setStatusCode(HttpStatus.SC_OK);
		InputStreamEntity body = new InputStreamEntity(in, length);
		body.setContentType(mimeType);
		res.setEntity(body);
	}

	protected void sendContent(HttpRequest req, HttpResponse res, final InputStream in,
			String mimeType) throws IOException {
		sendContent(req, res, in, mimeType, -1);
	}

	protected Session getSession(HttpContext context) {
		return (Session) context.getAttribute(SESSION_KEY);
	}

	protected void sendFile(HttpRequest req, HttpResponse res, File file,
			String contentType) throws IOException {
		res.setStatusCode(HttpStatus.SC_OK);

		FileEntity body = new FileEntity(file, contentType);

		res.setEntity(body);
	}

	protected void sendAsset(HttpRequest req, HttpResponse res, HttpContext context, String path,
			String contentType) throws UnsupportedEncodingException {
		// resource won't change until next release
		String relpath = (path.charAt(0) == '/' ? path.substring(1) : path);
		String etag = toHex(relpath + ":" + R.string.app_version);
		
		//for develop/debug
		//etag += System.currentTimeMillis();	
		sendAsset(req, res, context, relpath, contentType, etag);
	}
	
	protected void sendAsset(HttpRequest req, HttpResponse res, HttpContext context, String path,
			String contentType, String etag) throws UnsupportedEncodingException {
		//
		// AssetFileDescriptor fd = ctx.getAssets().openFd(name);
		// by fd, can't read if content is compressed
		//
		try {
			String relpath = (path.charAt(0) == '/' ? path.substring(1) : path);
			AssetManager am = ctx.getAssets();
			InputStream is = am.open(relpath);

			if (contentType != null && contentType.startsWith("text/")) {
				String s = toString(is);

				sendContent(req, res, context, s, contentType, "UTF-8", etag);
			} else {
				sendContent(req, res, is, contentType, etag);
			}

		} catch (Exception e) {
			sendError(req, res, e);
		}
	}

	protected void sendContent(HttpRequest req, HttpResponse res, HttpContext context, String s,
			String mimeType, String encoding, String etag) throws Exception {
		if (sentNotModified(req, res, mimeType, etag)) {
			return;
		}

		setCacheControlHeader(res);
		setETagHeader(res, etag);

		sendContent(req, res, context, s, mimeType, encoding);
	}

	protected void sendContent(HttpRequest req, HttpResponse res,
			InputStream is, String mimeType, String etag) throws Exception {
		if (sentNotModified(req, res, mimeType, etag)) {
			return;
		}

		setCacheControlHeader(res);
		setETagHeader(res, etag);
		sendContent(req, res, is, mimeType);
	}

	/**
	 * Send template html and append json object
	 * 
	 * @param req
	 * @param res
	 * @param name
	 * @param data
	 * @throws IOException
	 * @throws JSONException
	 */
	protected void sendAssetData(HttpRequest req, HttpResponse res,
			final String name, final JSONObject data) throws IOException,
			JSONException {
		final String s = toDataScript("data", data);
		final EntityTemplate body = new EntityTemplate(new ContentProducer() {
			public void writeTo(final OutputStream out) throws IOException {
				InputStream in = null;
				try {
					AssetManager am = ctx.getAssets();
					in = am.open(name);
					copy(in, out);
					out.flush();
					// append script
					out.write(s.getBytes());
					out.flush();
				} finally {
					close(in);
				}
			}
		});
		body.setContentType(CONTENT_TEXT_HTML);
		res.setEntity(body);
	}

	protected void close(Closeable in) throws IOException {
		try {
			in.close();
		} catch (Exception e) {
			//
		}
	}

	protected void sendRedirect(HttpResponse response, String uri)
			throws IOException {
		String type = getFormat(uri);
		String msg = "Redirect: " + uri;
		String s = format(STATUS_ + HttpStatus.SC_MOVED_TEMPORARILY, msg, type);

		StringEntity body = new StringEntity(s, type, "UTF-8");
		response.setStatusCode(HttpStatus.SC_MOVED_TEMPORARILY);
		response.addHeader("Location", uri);
		response.addHeader("Connection", "Close"); //
		
		response.setEntity(body);
	}

	protected void redirectLogin(HttpRequest req, HttpResponse res)
			throws IOException {

		sendRedirect(res, "/login");
	}

	protected void redirectHome(HttpRequest req, HttpResponse res)
			throws IOException {
		sendRedirect(res, "/home");
	}

	protected void copy(InputStream is, Appendable out) throws IOException {
		BufferedReader r = new BufferedReader(new InputStreamReader(is),
				BUFSIZE);
		while (true) {
			String l = r.readLine();
			if (l == null) {
				break;
			}
			out.append(l);
			out.append("\r\n");
		}
	}

	protected void copy(InputStream is, OutputStream os) throws IOException {
		byte[] buf = new byte[BUFSIZE];
		while (true) {
			int read = is.read(buf);
			if (read == -1) {
				break;
			}
			if (read > 0) {
				os.write(buf, 0, read);
			}
		}
		os.flush();
	}

	/**
	 * Protocol supported [internal], file, http, content
	 * 
	 * @param u
	 * @param out
	 * @param cleanup
	 * @throws IOException
	 */
	protected void copy(Uri u, File out, boolean cleanup) throws Exception {
		String scheme = u.getScheme();
		OutputStream os = null;
		InputStream is = null;
		try {
			os = new FileOutputStream(out);
			//
			if (TextUtils.isEmpty(scheme) || "internal".equals(scheme)) {
				Uri nu = Uri
						.parse(WebContentProvider.CONTENT_URI + u.getPath());
				is = ctx.getContentResolver().openInputStream(nu);
				// is = getLocalContent(u);
			} else if ("content".equals(scheme)) {
				is = ctx.getContentResolver().openInputStream(u);
			} else if ("file".equals(scheme)) {
				is = new FileInputStream(u.getPath());
			} else if ("http".equals(scheme)) {
				HttpGet get = new HttpGet(u.toString());
				HttpClient hc = new DefaultHttpClient();
				is = hc.execute(get).getEntity().getContent();
			} else {
				throw new IOException("Scheme not supported: " + u);
			}
			copy(is, os);
		} catch (Exception e) {
			if (cleanup) {
				out.delete();
			}
			throw e;
		} finally {
			close(is);
			close(os);
		}
	}

	protected String toString(InputStream is) throws IOException {
		StringBuffer sb = new StringBuffer();
		try {
			copy(is, sb);
		} finally {
			is.close();
		}
		return sb.toString();
	}

	protected InputStream format(Context ctx, String name, JSONObject data)
			throws Exception {
		InputStream is = null;
		AssetManager am = ctx.getAssets();
		is = am.open(name);
		String js = toDataScript("data", data);
		InputStream ds = new ByteArrayInputStream(js.getBytes());
		return new SequenceInputStream(is, ds);
	}

	protected String toDataScript(String var, JSONObject data)
			throws JSONException {
		StringBuilder sb = new StringBuilder();
		sb.append("\r\n<script type=\"text/javascript\">\r\n");
		sb.append("var " + var + " =\r\n");
		sb.append(data.toString(4));
		sb.append(";\r\n");
		sb.append("</script>");
		return sb.toString();
	}

	protected String getAsString(Context ctx, String name) throws IOException {
		InputStream is = null;
		try {
			AssetManager am = ctx.getAssets();
			is = am.open(name);
			String template = toString(is);
			return template;
		} finally {
			if (is != null) {
				is.close();
			}
		}
	}

	protected CharSequence findLabel(Context ctx, String pkg)
			throws NameNotFoundException {
		PackageManager pm = ctx.getPackageManager();
		ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
		return ai.loadLabel(pm);
	}

	@SuppressWarnings("unchecked")
	public static List<NVPair> getParams(HttpContext context) {
		List<NVPair> l = (List<NVPair>) context
				.getAttribute(BaseHandler.PARAMETERS_KEY);
		return l;
	}

	public static String getParam(List<NVPair> params, String name,
			String defval) {
		if (params == null) {
			return defval;
		}
		for (NVPair p : params) {
			if (name.equals(p.getName())) {
				return p.getValue();
			}
		}
		return defval;
	}

	protected static String formatRFC1123(long date) {
		String s = RFC1123_DF.format(new Date(date));
		return s;
	}

	protected void setLastModified(HttpResponse res, long lastModified) {
		// "Last-Modified: " + formatRFC1123(lastModified) + "\r\n";
		res.addHeader("Last-Modified", formatRFC1123(lastModified));
	}

	public boolean handle(String path, HttpRequest request,
			HttpResponse response, HttpContext context) throws Exception {
		return false;
	}

	protected String guessContentType(String name, String deftype) {
		// remove .cmf before guessing
		if (name.endsWith(Files.CAMOUFLAGE)) {
			int idx = name.lastIndexOf(".");
			name = name.substring(0, idx);
		}
		String type = Files.guessContentType(name, deftype);
		if (Debug.DEBUG) {
			Log.d(TAG, " " + type);
		}
		return type;
	}

	/**
	 * Camouflaged temporary file
	 * 
	 * @param pathname
	 * @return
	 */
	protected File getCacheFile(String pathname) {
		return Files.camouflage(ctx, pathname);
	}

	/**
	 * 
	 * @param req
	 * @param res
	 * @param data
	 * @return text/plain if client does not accept application/json to prevent download dialog (IE and Firefox) for Ajax calls
	 * 
	 * @throws Exception
	 */
	protected StringEntity createJsonEntity(HttpRequest req, HttpResponse res, HttpContext context, String data) throws Exception {
		Header h = req.getFirstHeader("Accept");
		String v = h.getValue();
		Object fix = context.getAttribute(FIX_JSON_KEY);
		boolean fixJson = (fix != null && fix.equals("true"));
		if ((v == null || v.indexOf(APPLICATION_JSON) == -1) && fixJson) {
			return new StringEntity(data, TEXT_PLAIN, UTF8);
		}
		return new StringEntity(data, APPLICATION_JSON, UTF8);
	}
	
	protected void sendJSONData(HttpRequest req, HttpResponse res, HttpContext context,
			Object data) throws Exception {
		//StringEntity body = new StringEntity(data.toString(), APPLICATION_JSON, UTF8);
		StringEntity body = createJsonEntity(req, res, context, data.toString());
		res.setStatusCode(HttpStatus.SC_OK);
		res.setEntity(body);
	}

	protected void sendJSONData(HttpRequest req, HttpResponse res, HttpContext context,
			String callback, Object data) throws Exception {
		if (TextUtils.isEmpty(callback)) {
			sendJSONData(req, res, context, data);
		} else {
			String call = callback + "(" + data.toString() + ")";
			//StringEntity body = new StringEntity(call, APPLICATION_JSON, UTF8);
			StringEntity body = createJsonEntity(req, res, context, call);
			res.setStatusCode(HttpStatus.SC_OK);
			res.setEntity(body);
		}
	}

	protected void sendScript(HttpRequest req, HttpResponse res, String var,
			JSONObject data) throws Exception {

		StringBuilder sb = new StringBuilder();
		sb.append("var " + var + " =\r\n");
		sb.append(data.toString(4));
		sb.append(";\r\n");

		StringEntity body = new StringEntity(sb.toString(), TEXT_JAVASCRIPT,
				UTF8);
		res.setStatusCode(HttpStatus.SC_OK);
		res.setEntity(body);
	}

	protected void sendDrawable(HttpRequest req, HttpResponse res, String name,
			int resId, String type) throws Exception {

		final String pathname = "/drawable/" + name;
		final File f = getCacheFile(pathname);
		if (!f.exists()) {
			if (resId != 0) {
				ResourceUtils.copyRes(ctx, resId, f);
			} else {
				throw new NotFoundException(req.getRequestLine().getUri());
			}
		}

		sendFile(req, res, f, type);
	}

	/**
	 * scheme/auth/path
	 * 
	 * @param u
	 * @return
	 */
	protected String cachePath(Uri u) {
		return Files.cachePath(u);
	}

	protected int parseInt(String s, int def) {
		try {
			return Integer.parseInt(s);
		} catch (Exception e) {
			//
		}
		return def;
	}

	protected void setCacheControlHeader(HttpResponse res) {
		final long ten_thousand_years = 10000 * 365 * 24 * 60 * 60;
		long age = System.currentTimeMillis() / 1000 + ten_thousand_years;
		setCacheControlHeader(res, age);
	}

	protected void setCacheControlHeader(HttpResponse res, long age) {
		res.setHeader("Cache-Control", "max-age=" + age);
	}

	protected void setETagHeader(HttpResponse res, String etag) {
		res.setHeader("ETag", "\"" + etag + "\"");
	}

	protected static String toHex(String s) {
		byte[] ba = s.getBytes();
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < ba.length; i++) {
			String hex = Integer.toHexString(ba[i] & 0x000000ff);
			int len = hex.length();
			while (len++ < 2) {
				sb.append("0");
			}
			sb.append(hex);
		}
		return sb.toString();
	}

	protected boolean sentNotModified(HttpRequest req, HttpResponse res,
			String mimeType, String etag) {
		Header hd = req.getFirstHeader("If-None-Match");
		if (hd == null) {
			return false;
		}
		String qtag = hd.getValue();
		if (Debug.DEBUG) {
			Log.d(TAG, "sentNotModified If-None-Match: " + qtag + " : " + etag);
		}
		if (!TextUtils.isEmpty(qtag)
				&& (qtag.startsWith("\"" + etag + "\"") || qtag
						.startsWith(etag))) {
			res.setHeader("Content-Type", mimeType + "; charset=UTF-8");
			res.setStatusCode(HttpStatus.SC_NOT_MODIFIED);

			return true;
		}
		return false;
	}
}
